/*
Copyright (C) BABEC. All rights reserved.
Copyright (C) THL A29 Limited, a Tencent company. All rights reserved.

SPDX-License-Identifier: Apache-2.0
*/

package command

import (
	"fmt"
	"io/ioutil"
	"path/filepath"
	"strings"
	"time"

	"chainmaker.org/chainmaker-cryptogen/config"
	"chainmaker.org/chainmaker/common/v3/crypto"
	"chainmaker.org/chainmaker/common/v3/crypto/asym"
	"chainmaker.org/chainmaker/common/v3/helper"
	pbconfig "chainmaker.org/chainmaker/pb-go/v3/config"
	"chainmaker.org/chainmaker/utils/v3"
	"github.com/spf13/cobra"
)

func GeneratePKCmd() *cobra.Command {
	generateCmd := &cobra.Command{
		Use:   "generate-pk",
		Short: "Generate public-key material",
		Long:  "Generate public-key material",
		RunE: func(cmd *cobra.Command, args []string) error {
			return generatePK()
		},
	}
	generateCmd.Flags().StringVarP(&pkOutputDir, "output", "o", "crypto-config", "specify the output directory in which to place key")
	return generateCmd
}

func generatePK() error {
	config.LoadPKConfig(ConfigPath)
	pkConfig := config.GetPKConfig()

	for _, item := range pkConfig.Item {
		keyType := crypto.AsymAlgoMap[strings.ToUpper(item.PKAlgo)]
		hashType := crypto.HashAlgoMap[strings.ToUpper(item.HashAlgo)]

		nodePath := filepath.Join(pkOutputDir)

		privKeyPemList := make([]string, item.Admin.Count)
		pubKeyPemList := make([]string, item.Admin.Count)
		for i := 0; i < int(item.Admin.Count); i++ {
			privKeyPEM, pubKeyPEM, err := generatePKAdmin(keyType)
			if err != nil {
				return err
			}
			privKeyPemList[i] = privKeyPEM
			pubKeyPemList[i] = pubKeyPEM
		}

		for _, node := range item.Node {
			for j := 0; j < int(node.Count); j++ {
				name := fmt.Sprintf("%s%d", "node", j+1)
				path := filepath.Join(nodePath, name)
				if err := generatePKNode(path, name, keyType, hashType); err != nil {
					return err
				}
				for _, user := range item.User {
					for i := 0; i < int(user.Count); i++ {
						name := fmt.Sprintf("%s%d", user.Type, i+1)
						path := filepath.Join(path, "user", name)
						if err := generatePKUser(path, name, keyType, item.HashAlgo); err != nil {
							return err
						}
					}
				}

				adminName := fmt.Sprintf("%s%d", "node", j+1)
				adminPath := filepath.Join(nodePath, adminName)
				for i := 0; i < int(item.Admin.Count); i++ {
					fileName := fmt.Sprintf("%s%d", "admin", i+1)
					filePath := filepath.Join(adminPath, "admin", fileName)
					err := savePKAdmin(filePath, fileName, []byte(privKeyPemList[i]), []byte(pubKeyPemList[i]))
					if err != nil {
						return err
					}

				}

				time.Sleep(time.Millisecond * 50)
			}
		}
	}

	return nil
}

func savePKAdmin(filePath, fileName string, privKeyPEM, pubKeyPEM []byte) error {

	keyName := fmt.Sprintf("%s%s", fileName, ".key")
	pemName := fmt.Sprintf("%s%s", fileName, ".pem")
	if err := makeFile(filePath, keyName); err != nil {
		return err
	}

	if err := ioutil.WriteFile(
		filepath.Join(filePath, keyName), privKeyPEM, 0600,
	); err != nil {
		return fmt.Errorf("save key to file [%s] failed, %s", filePath, err.Error())
	}

	if err := makeFile(filePath, pemName); err != nil {
		return err
	}

	if err := ioutil.WriteFile(
		filepath.Join(filePath, pemName), pubKeyPEM, 0600,
	); err != nil {
		return fmt.Errorf("save key to file [%s] failed, %s", filePath, err.Error())
	}

	return nil
}

func generatePKAdmin(keyType crypto.KeyType) (string, string, error) {
	return asym.GenerateKeyPairPEM(keyType)
}

func generatePKUser(filePath, fileName string, keyType crypto.KeyType, hashType string) error {
	keyName := fmt.Sprintf("%s.key", fileName)
	pemName := fmt.Sprintf("%s.pem", fileName)

	priv, err := asym.GenerateKeyPair(keyType)
	if err != nil {
		return fmt.Errorf("GenerateKey failed, %s", err.Error())
	}
	privKeyPEM, err := priv.String()
	if err != nil {
		return err
	}
	pubKeyPEM, err := priv.PublicKey().String()
	if err != nil {
		return err
	}

	if err = makeFile(filePath, keyName); err != nil {
		return err
	}

	if err = ioutil.WriteFile(
		filepath.Join(filePath, keyName), []byte(privKeyPEM), 0600,
	); err != nil {
		return fmt.Errorf("save key to file [%s] failed, %s", filePath, err.Error())
	}

	if err = makeFile(filePath, pemName); err != nil {
		return err
	}

	if err = ioutil.WriteFile(
		filepath.Join(filePath, pemName), []byte(pubKeyPEM), 0600,
	); err != nil {
		return fmt.Errorf("save key to file [%s] failed, %s", filePath, err.Error())
	}

	// calc client address for DPoS using client's sign cert
	cryptoHashType := crypto.HashAlgoMap[hashType]

	// use chainmaker-utils addr cal func
	addr, err := utils.PkToAddrStr(priv.PublicKey(), pbconfig.AddrType_ETHEREUM, cryptoHashType)

	userAddrFileName := filepath.Join(filePath, fmt.Sprintf("%s.addr", fileName))
	err = ioutil.WriteFile(userAddrFileName, []byte(addr), 0600)
	if err != nil {
		return err
	}

	return nil
}

func generatePKNode(filePath, fileName string, keyType crypto.KeyType, hashType crypto.HashType) error {
	keyName := fmt.Sprintf("%s.key", fileName)
	pemName := fmt.Sprintf("%s.pem", fileName)

	privKeyPEM, pubKeyPEM, err := asym.GenerateKeyPairPEM(keyType)
	if err != nil {
		return fmt.Errorf("GenerateKeyPairPEM failed, %s", err.Error())
	}

	if err = makeFile(filePath, keyName); err != nil {
		return err
	}

	if err = ioutil.WriteFile(
		filepath.Join(filePath, keyName), []byte(privKeyPEM), 0600,
	); err != nil {
		return fmt.Errorf("save key to file [%s] failed, %s", filePath, err.Error())
	}

	if err = makeFile(filePath, pemName); err != nil {
		return err
	}

	if err = ioutil.WriteFile(
		filepath.Join(filePath, pemName), []byte(pubKeyPEM), 0600,
	); err != nil {
		return fmt.Errorf("save key to file [%s] failed, %s", filePath, err.Error())
	}

	publicKey, err := asym.PublicKeyFromPEM([]byte(pubKeyPEM))
	if err != nil {
		return err
	}

	nodeUid, err := helper.CreateLibp2pPeerIdWithPublicKey(publicKey)
	if err != nil {
		return err
	}
	nodeIdFileName := filepath.Join(filePath, fmt.Sprintf("%s.nodeid", fileName))
	return ioutil.WriteFile(nodeIdFileName, []byte(nodeUid), 0600)
}
